package com.billy.ohos.loading;

import ohos.aafwk.ability.Ability;
import ohos.agp.components.Component;
import ohos.agp.components.ComponentContainer;
import ohos.agp.components.ComponentParent;
import ohos.agp.components.DirectionalLayout;
import ohos.agp.components.StackLayout;
import ohos.app.Context;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.utils.PlainArray;

import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * manage loading status view<br>
 * usage:<br>
 * //if set true, logs will print into logcat<br>
 * Gloading.debug(trueOrFalse);<br>
 * <p>
 * //init the default loading status view creator ({@link Adapter})<br>
 * Gloading.initDefault(adapter);<br>
 * <p>
 * //wrap an activity. return the holder<br>
 * Holder holder = Gloading.getDefault().wrap(activity);<br>
 * <p>
 * //wrap an activity and set retry task. return the holder<br>
 * Holder holder = Gloading.getDefault().wrap(activity).withRetry(retryTask);<br>
 * <p>
 * //wrap a view and set retry task. return the holder<br>
 * Holder holder = Gloading.getDefault().wrap(view).withRetry(retryTask);<br>
 * <p>
 * //cover a view which relatives to another view within a parent of RelativeLayout or ConstraintLayout parent
 * Holder holder = Gloading.getDefault().cover(view).withRetry(retryTask);<br>
 * <br>
 * holder.showLoading() //show loading status view by holder<br>
 * holder.showLoadSuccess() //show load success status view by holder (frequently, hide gloading)<br>
 * holder.showFailed() //show load failed status view by holder (frequently, needs retry task)<br>
 * holder.showEmpty() //show empty status view by holder. (load completed, but data is empty)
 *
 * @author billy.qi
 * @since 19/3/18 17:49
 */
public class Gloading {

    public static final int STATUS_LOADING = 1;
    public static final int STATUS_LOAD_SUCCESS = 2;
    public static final int STATUS_LOAD_FAILED = 3;
    public static final int STATUS_EMPTY_DATA = 4;

    private static volatile Gloading mDefault;
    private Adapter mAdapter;
    private static boolean DEBUG = false;

    /**
     * Provides view to show current loading status
     */
    public interface Adapter {
        /**
         * get view for current status
         *
         * @param holder           Holder
         * @param convertComponent The old component to reuse, if possible.
         * @param status           current status
         * @return status view to show. Maybe convertView for reuse.
         * @see Holder
         */
        Component getComponent(Holder holder, Component convertComponent, int status);
    }

    /**
     * set debug mode or not
     *
     * @param debug true:debug mode, false:not debug mode
     */
    public static void debug(boolean debug) {
        DEBUG = debug;
    }

    private Gloading() {
    }

    /**
     * Create a new Gloading different from the default one
     *
     * @param adapter another adapter different from the default one
     * @return Gloading
     */
    public static Gloading from(Adapter adapter) {
        Gloading gloading = new Gloading();
        gloading.mAdapter = adapter;
        return gloading;
    }

    /**
     * get default Gloading object for global usage in whole app
     *
     * @return default Gloading object
     */
    public static Gloading getDefault() {
        if (mDefault == null) {
            synchronized (Gloading.class) {
                if (mDefault == null) {
                    mDefault = new Gloading();
                }
            }
        }
        return mDefault;
    }

    /**
     * init the default loading status view creator ({@link Adapter})
     *
     * @param adapter adapter to create all status views
     */
    public static void initDefault(Adapter adapter) {
        getDefault().mAdapter = adapter;
    }

    /**
     * Gloading(loading status view) wrap the whole ability
     *
     * @param ability          current ability
     * @param contentComponent current ability contentComponent
     * @return holder of Gloading
     */
    public Holder wrap(Ability ability, Component contentComponent) {
        ComponentContainer wrapper = (ComponentContainer) contentComponent.getComponentParent();
        return new Holder(mAdapter, ability, wrapper);
    }

    /**
     * Gloading(loading status view) wrap the specific view.
     *
     * @param component component to be wrapped
     * @return Holder
     */
    public Holder wrap(Component component) {
        StackLayout wrapper = new StackLayout(component.getContext());
        ComponentContainer.LayoutConfig lp = component.getLayoutConfig();

        if (lp != null) {
            wrapper.setLayoutConfig(lp);
        }
        if (component.getComponentParent() != null) {
            ComponentContainer parent = (ComponentContainer) component.getComponentParent();
            int index = parent.getChildIndex(component);
            parent.removeComponent(component);
            parent.addComponent(wrapper, index);
        }
        StackLayout.LayoutConfig newLp = new StackLayout.LayoutConfig(StackLayout.LayoutConfig.MATCH_PARENT, StackLayout.LayoutConfig.MATCH_PARENT);
        wrapper.addComponent(component, newLp);
        return new Holder(mAdapter, component.getContext(), wrapper);
    }

    /**
     * loadingStatusView shows cover the view with the same LayoutParams object
     * this method is useful with RelativeLayout and ConstraintLayout
     *
     * @param component the component which needs show loading status
     * @return Holder
     */
    public Holder cover(Component component) {
        ComponentParent parent = component.getComponentParent();
        if (parent == null) {
            throw new RuntimeException("view has no parent to show gloading as cover!");
        }
        ComponentContainer componentContainer = (ComponentContainer) parent;
        DirectionalLayout wrapper = new DirectionalLayout(component.getContext());
        componentContainer.addComponent(wrapper, component.getLayoutConfig());
        return new Holder(mAdapter, component.getContext(), wrapper);
    }

    /**
     * Gloading holder<br>
     * create by {@link Gloading#( Ability )} or {@link Gloading#wrap(Component)}<br>
     * the core API for showing all status view
     */
    public static class Holder {
        private Adapter mAdapter;
        private Context mContext;
        private Runnable mRetryTask;
        private Component mCurStatusComponent;
        private ComponentContainer mWrapper;
        private int curState;
        private PlainArray<Component> mStatusComponents = new PlainArray<>(4);
        private Object mData;

        private Holder(Adapter adapter, Context context, ComponentContainer wrapper) {
            this.mAdapter = adapter;
            this.mContext = context;
            this.mWrapper = wrapper;
        }

        /**
         * set retry task when user click the retry button in load failed page
         *
         * @param task when user click in load failed UI, run this task
         * @return this
         */
        public Holder withRetry(Runnable task) {
            mRetryTask = task;
            return this;
        }

        /**
         * set extension data
         *
         * @param data extension data
         * @return this
         */
        public Holder withData(Object data) {
            this.mData = data;
            return this;
        }

        /**
         * show UI for status: {@link #STATUS_LOADING}
         */
        public void showLoading() {
            showLoadingStatus(STATUS_LOADING);
        }

        /**
         * show UI for status: {@link #STATUS_LOAD_SUCCESS}
         */
        public void showLoadSuccess() {
            showLoadingStatus(STATUS_LOAD_SUCCESS);
        }

        /**
         * show UI for status: {@link #STATUS_LOAD_FAILED}
         */
        public void showLoadFailed() {
            showLoadingStatus(STATUS_LOAD_FAILED);
        }

        /**
         * show UI for status: {@link #STATUS_EMPTY_DATA}
         */
        public void showEmpty() {
            showLoadingStatus(STATUS_EMPTY_DATA);
        }

        /**
         * Show specific status UI
         *
         * @param status status
         * @see #showLoading()
         * @see #showLoadFailed()
         * @see #showLoadSuccess()
         * @see #showEmpty()
         */
        public void showLoadingStatus(int status) {
            if (curState == status || !validate()) {
                return;
            }
            curState = status;
            // first try to reuse status view\
            Component convertConponent = null;
            if (mStatusComponents != null) {
                if (mStatusComponents.size() > 0) {
                    if (mStatusComponents.get(status).isPresent()) {
                        convertConponent = mStatusComponents.get(status).get();
                    }
                }
            }
            if (convertConponent == null) {
                // secondly try to reuse current status view
                convertConponent = mCurStatusComponent; // Loading
            }
            try {
                // call customer adapter to get UI for specific status. convertView can be reused
                Component component = mAdapter.getComponent(this, convertConponent, status);
                if (component == null) {
                    printLog(mAdapter.getClass().getName() + ".getView returns null");
                    return;
                }
                if (component != mCurStatusComponent || mWrapper.getChildIndex(component) < 0) {
                    if (mCurStatusComponent != null) {
                        mWrapper.removeComponent(mCurStatusComponent);
                    }
                    ComponentContainer.LayoutConfig lp = component.getLayoutConfig();
                    if (lp != null) {
                        lp.width = ComponentContainer.LayoutConfig.MATCH_PARENT;
                        lp.height = ComponentContainer.LayoutConfig.MATCH_PARENT;
                    }
                    mWrapper.addComponent(component);
                } else if (mWrapper.getChildIndex(component) != mWrapper.getChildCount() - 1) {
                    // make sure loading status component at the front
                    if (component.getComponentParent() != null) {
                        component.getComponentParent().moveChildToFront(component);
                    }
                }
                mCurStatusComponent = component;
                mStatusComponents.put(status, component);
            } catch (Exception e) {
                if (DEBUG) {
                    Logger.getLogger(Gloading.class.getName()).log(Level.WARNING, e.getMessage());
                }
            }
        }

        private boolean validate() {
            if (mAdapter == null) {
                printLog("Gloading.Adapter is not specified.");
            }
            if (mContext == null) {
                printLog("Context is null.");
            }
            if (mWrapper == null) {
                printLog("The mWrapper of loading status view is null.");
            }
            return mAdapter != null && mContext != null && mWrapper != null;
        }

        /**
         * get Context
         *
         * @return Context
         */
        public Context getContext() {
            return mContext;
        }

        /**
         * get wrapper
         *
         * @return container of gloading
         */
        public ComponentContainer getWrapper() {
            return mWrapper;
        }

        /**
         * get retry task
         *
         * @return retry task
         */
        public Runnable getRetryTask() {
            return mRetryTask;
        }

        /**
         * get extension data
         *
         * @param <T> return type
         * @return data
         */
        public <T> T getData() {
            try {
                return (T) mData;
            } catch (Exception e) {
                if (DEBUG) {
                    Logger.getLogger(Gloading.class.getName()).log(Level.WARNING, e.getMessage());
                }
            }
            return null;
        }
    }

    private static void printLog(String msg) {
        if (DEBUG) {
            HiLog.info(new HiLogLabel(HiLog.LOG_APP, 0, "Gloading"), msg);
        }
    }

}
